- 81 - Г Л А В А 4 (Андрэ Ла Мот) ------------------- МЕХАНИЗМЫ ДВУХМЕРНОЙ ГРАФИКИ Вы можете считать, что экран компьютера подобен листу бумаги, который можно изгибать, сворачивать в трубочку и вообще как угодно трансформировать в руках. Чтобы делать подобные вещи, мы должны знать язык, с помощью которого все это выполняется - язык математики. В данной главе мы познакомимся с тем, что называется двухмерная проекция и изучим следующие темы: - Картезианская или двухмерная проекция; - Точки, линии и области; - Трансляция; - Масштабирование; - Повороты; - Разрезание; - Использование матриц. ДВУХМЕРНАЯ ПРОЕКЦИЯ Двухмерную проекцию (2-D), иначе называемую картезианской, можно сравнить с листом бумаги в клеточку. Рис.4.1 показывает это. - 82 - ось Y ┼ ┼ ┼ ┼ (-3,4) ┼ . ┼ ┼ ┼ (1,1) ось X ┼ . ─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─ ┼ ┼ ┼ ┼ ┼ ┼ Рис.4.1. Точки (1,1) и (-3,4) на двухмерной проекции. Каждая точка в двухмерной проекции однозначно описывается двумя координатами. Обычно эти координаты обозначаются буквами x и y, где x определяет точку на горизонтальной оси X, а y задает точку на вертикальной оси Y. Например, если нам захочется нарисовать точки (1,1) и (-3,4), то мы сделаем так, как показано на рис.4.1. Начав с изучения свойств двухмерной проекции, мы будем постепенно двигаться от простого к сложному, и к тому времени, когда дойдем до трехмерных объектов, они не покажутся чересчур сложными. Кроме того, существует ведь немало весьма привлекательных двухмерных игр, о которых также не стоит забывать. Тем важнее понять механизмы получения двухмерной графики и рассмотреть алгоритмы различных графических преобразований. ТОЧКИ, ЛИНИИ И ОБЛАСТИ Все мы видели игры типа Asteroids, Spectre и Major Havoc. Многие из них имеют общие черты первых видеоигр - все они выполнены линиями и все они, как правило, плоские. Кстати, в предыдущих главах мы еще ничего не делали для рисования проекции кроме отображения точки. - 83 - Точки --------------- Мы уже дали определение точке. Она представляет собой позицию на плоскости, которую можно описать парой координат х и у. Давайте напишем маленькую программу на Си, рисующую точки на экране. Листинг 4.1 показывает такую программу. Листинг 4.1. Программа, рисующая точки (POINTY.C). ------------------------------------------------------------------------- #include // include the basics #include // include Microsofts Graphics Header void main(void) { int x,y,index,color; // put the computer into graphics mode _setvideomode(_VRES16COLOR); // 640x480 in 16 colors // let's draw 10000 points randomly on the screen for (index = 0; index<10000; index++) { // get a random position and color and plot a point there x = rand()%640; y = rand()%480; color = rand()%16; _setcolor(color); // set the color of the pixel to be drawn _setpixel(x,y); // draw the pixel } // end for index // wait for the user to hit a key while(!kbhit()){} // place the computer back into text mode _setvideomode(_DEFAULTMODE); } // end main -------------------------------------------------------------------------- Теперь разберемся, что делает эта программа: - Компьютер переводится в режим VGA с помощью вызова функции Си _setvideomode(_VRES16COLOR0. Эта функция из графической библиотеки Microsoft. После этого программа входит в главный цикл. В структуре - 84 - FOR каждый раз случайным образом генерируются 3 числа: одно для цвета и 2 других для коотдинат (х,у) позиции точки, которую мы хотим нарисовать; - Затем мы используем библиотечную функцию _septixel(x,y), чтобы нарисовать точку на экране. Программа делает это 10000 раз, а потом останавливается; - Затем программа ждет нажатия любой клавиши, после чего происходит выход их DOS. Если вы запустите программу несколько раз, то сможете заметить , что точки все время оказываются в одних и тех же местах. Как это получается? Дедо в том, что мы пользуемся функцией rand(), которая не является в полном смысле генератором случайных чисел. Она возвращает так называемые псевдослучайные числа. Чтобы избежать этого, вам надо всякий раз при запуске устанавливать генератор случайных чисел с разными начальными значениями. Вставьте в начало программы функцию stand(int) - и все будет в порядке. Линии -------------- Линия, как вы знаете, - это кратчайший отрезок между двумя точками. Например, между точками (1,1) и (5,5) на плоскости линия будет выглядеть так (рис.4.2): ось Y (5,5) ─┼─ . ─┼─ ─┼─ ─┼─ ─┼─. (1,1) ось X ┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─── ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ Рис.4.2. Линия на плоскости - 85 - Давайте изменим программу из Листинга 4.1 так, чтобы она рисовала линии вместо точек.Чтобы сделать это, нужно кое-что изменить. Вместо 2 случайных чисел х и у теперь их будет 4: (х1,у1) и (х2,у2). Потом программа нарисует между ними линию, используя вызов боблиотечной функции. Листинг будет выглядеть так: Листинг 4.2. Программа, рисующая линии (LINER.C). ------------------------------------------------------------------------- #include // include the basics #include // include Microsofts Graphics Header void main(void) { int x1,y1,x2,y2,color,index; // put the computer into graphics mode _setvideomode(_VRES16COLOR); // 640x480 in 16 colors // let's draw 1,000 lines randomly on the screen for (index = 0; index<1000; index++) { // get a random positions and color and draw a line there x1 = rand()%640; // x of starting point y1 = rand()%480; // y of starting point x2 = rand()%640; // x of ending point y2 = rand()%480; // y of ending point color = rand()%16; _setcolor(color); // set the color of the pixel to be drawn _moveto(x1,y1); // move to the start of the line _lineto(x2,y2); // draw the line } // end for index // wait for the user to hit a key while(!kbhit()){} // place the computer back into text mode _setvideomode(_DEFAULTMODE); } // end main ------------------------------------------------------------------------- - 86 - Многоугольники -------------------- Линии весьма просты, и если вы приложите немного усилий, то сможете из программы 4.2 сделать простой Screen Saver. Но видеоигры кроме линий содержат еще множество интересных графических объектов, например, многоугольников. Многоугольник - это множество точек, объединенных линиями. Точки пересечения линий называются вершинами многоугольника. На рис.4.3 показан треугольник, образованный тремя вершинами. ось Y │ . вершина 1 ─┼─ (4,13) ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼ ось Х ─────────┼─┼─┼─┼─┼─┼─┼─┼─┼─┬─┤─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─ ┼ вершина 3 ─┼─ (-6,-3) . ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ ─┼─ │ . вершина 2 │ (10,-9) Рис.4.3. Пример простого многоугольника. Все многоугольники являются закрытыми геометрическими объектами. Многоугольники бывают двух типов. Оба типа представлены на рис.4.4. Существует математический алгоритм, позволяющий определить, к какому типу относится данный многоугольник, но он довольно сложен и сейчас нам не нужен. Давайте лучше напишем программу, рисующую многоугольники (Листинг 4.3). - 87 - ось Y │ ─┼─ ─┼─ ┌─────────────────┐ ┌────────────── ┼─ │ │ │ ─┼─ │ │ │ ─┼─ │ │ │ ─┼─ │ │ ось Х ───┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼──┼──┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─ │ ∙ ─┼─ │ │ │ ─┼─ │ │ выгнутый вогнутый ─── │ ┼─ │ │ ───(выпуклый) └─────────────── ─┼─ │ │ ─┼─ └─────────────────┘ │ │ Рис.4.4. Два типа многоугольников Листинг 4.3. Программа, рисующая многоугольники (POLYDRAW.C). ------------------------------------------------------------------------- #include // include the basics #include // include Microsofts Graphics Header void main(void) { // put the computer into graphics mode _setvideomode(_VRES16COLOR); // 640x480 in 16 colors // draw a simple polygon _setcolor(1); // blue _moveto(100,100); // vertex 1 _lineto(120,120); // vertex 2 _lineto(150,200); // vertex 3 _lineto(80,190); // vertex 4 _lineto(80,60); // vertex 5 _lineto(100,100); // back to vertex 1 to close up the polygon // now highlight each vertex in white _setcolor(15); // white _setpixel(100,100); // vertex 1 _setpixel(120,120); // vertex 2 _setpixel(150,200); // vertex 3 _setpixel(80,190); // vertex 4 _setpixel(80,60); // vertex 5 // wait for the user to hit a key while(!kbhit()){} // place the computer back into text mode _setvideomode(_DEFAULTMODE); } // end main _________________________________________________________________________ - 88 - ОБЪЕКТЫ Продвинемся немного дальше и определим структуру объекта. Если мы хотим использовать линии для рисования кораблей и планет, то нам необходима структура данных для хранения атрибутов этих объектов. Для начала постараемся ее не осложнять. Пусть каждый наш объект имеет не более 16 вершин, цвет и конкретное местоположение на экране. Как вычислять позицию, мы узнаем позже, а код для описания вершин в структуре данных будет выглядеть примерно так: Листинг 4.4. Структуры данных для задания вершин объектов. ------------------------------------------------------------------------- typedef struct vertex_typ { float x,y; // a single point in the 2-D plane. } vertex, *vertex_ptr; // the structure for an object typedef struct object_typ { int num_vertices; // number of vertices in this object int color; // color of object float xo,yo; // position of object float x_velocity; // x velocity of object float y_velocity; // y velocity of object float scale; // scale factor float angle; // rotation rate vertex vertices[16]; // 16 vertices } object, *object_ptr; __________________________________________________________________________ - 89 - Эта структура данных нам нужна для описания объекта, представляющего собой многоугольник определенного цвета и расположенного в определенной позиции на экране. Позиционирование объекта --------------------------- Теперь поговорим о строчке, которая определяет позицию объекта в структуре из Литстинга 4.4. Координаты (хо,уо) описывают начальную позицию объекта на плоскости. Многоугольник или объект рисуется относительно начальной позиции. Все это подводит нас к понятию относительной системы координат. Возможно, вы не знаете, что картезианская система называется еще мировой системой координат. Подразумевается, что она просто огромна. В то же самое время экран ПК имеет свою систему координат, называемую экранной. При этом все объекты на экране имеют свою, локальную систему координат. Это показано на рис.4.5. Мы можем определить объект в локальной системе координат, затем преобразовать эти координаты в мировые и,наконец, нарисовать наш объект в экранной системе координат. Выглядит это достаточно долгим занятием, но на ┌────────────────────────────────────────────────────┐ │ ┌─────────────────────────────────────────────> │ │ │(0,0) │ (320,0) │ │ │ │ │ │ │ (-5,5) │ (5,5) │ │ │ ┌────┼────┐ │ │ │ │ ┼(0,0) │ │ │ ─────────┬┬┼┬┬┬┬┼┬┬┬┬┼┬───────── │ │ │ │ │ │ │ │ │ └────┼────┘ │ │ │ (-5,-5) │ (5,-5) │ │ │ │ │ │ (0,200) │<-- Видеоэкран │ │ └────────────────────────────────────────────────────┘ Рис.4.5. Локальная система координат объекта на экране. - 90 - ┌────────────────────────────────────────────────────┐ │ ^ (0,200) │ │ │Y │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │<-- Видеоэкран │ │(0,0) (320,0) │ │ └────────────────────────────────────────────> X │ └────────────────────────────────────────────────────┘ Рис.4.6. Переворачивание экрана для достижения полного соответствия с 1-м квадрантом картезианской системы. деле все оказывается несколько проще. Давайте договоримся, что у нас на компьютере мировые и экранные системы координат совпадают. Это значит, что: - Точка (0,0) находится в левом верхнем углу экрана; - При движении вправо увеличивается значение Х-координаты; - При перемещении вниз увеличивается Y-координата. Благодаря этим допущениям мы получаем экранные координаты, похожие на координаты 1-го квадранта (положительные значения осей Х и Y), но при этом надо всегда помнить, что ось Y у нас перевернута относительно экрана. В принципе, в этом нет ничего страшного, хотя и несколько непривычно. Чтобы чувствовать себя уверенно, перевернем ось Y в нормальное положение. Тогда точка (0,0) будет находиться в левом нижнем углу экрана, как это показано на рис.4.6. Теперь у нас есть все средства для представления объектов в компьютере. В следующем разделе мы обсудим, как перемещать объекты по экрану, поворачивать и масштабировать их. Трансляция объектов ----------------------- Трансляцией объекта будем называть его перемещение, при котором не меняется ни угол поворота, ни размер объекта.Давайте воспользуемся нашей структурой - 91 - ось Y │ (-3.5, 5.5)┼ . ─┼─ ─┼─ ─┼─ (4, 3.5) ─┼─ . ─┼─ ─┼─ ось Х ──┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼──── ─┼─ ─┼─ ─┼─ . .(8.5, 3) ─┼─(2,-3) ─┼─ . (6, -5) . ─┼─ (-4,-6) │ Рис.4.7. Многоугольник, определяющий вид нашего астероида данных для определения конкретного объекта, с которым будем экспериментировать и в дальнейшем. К примеру, пусть это будет астероид. На рис.4.7 показан его внешний вид. Листинг 4.5 содержит фрагмент, описывающий наш астероид. Листинг 4.5. Описание астероида. ------------------------------------------------------------------------- Object asteroid; // определим поля asteroid.num_vertices = 6; // шести вершин будет достаточно asteroid.color = 1; // цвет астероида - синий astreoid.x0 = 320; // поместить астероид в центр экрана asteroid.y0 = 240; // теперь задаем координаты вершин как смещения относительно // точки х0, у0. asteroid.vertices[0].x = 4.0; asteroid.vertices[0].y = 3.5; asteroid.vertices[1].x = 8.5; asteroid.vertices[1].y = -3.0; asteroid.vertices[2].x = 6.0; asteroid.vertices[2].y = -5.0; asteroid.vertices[3].x = 2.0; asteroid.vertices[3].y = -3.0; asteroid.vertices[4].x = -4.0; asteroid.vertices[4].y = -6.0; asteroid.vertices[5].x = -3.5; asteroid.vertices[5].y = 5.5; ------------------------------------------------------------------------- - 92 - Конечно, в настоящих играх вам не придется так определять все свои объекты. Напротив, вы можете загрузить координаты вершин из файла или сгенерировать их (например, AutoCad использует формат DXF, содержащий списки вершин вместе с другими свойствами объекта; после того как DXF-файл загружен, координаты вершин считываются из него в соответствующие структуры). Но поскольку мы создаем всего один астероид, то можно описать его и вручную. Теперь давайте чуть-чуть подумаем. Мы можем нарисовать вершины объекта относительно его положения на экране, которое описывается как (хо,уо). Если же мы хотим передвинуть объект, то можно сделать так: хо=хо+dх уо=уо+dу где: dх и dу - это количество пикселей, на которое мы хотим переместить объект по оси X или Y. Это все, что можно сказать о трансляции объектов. Теперь поговорим о масштабировании. Масштабирование объектов ----------------------------- Масштабирование означает изменение размера объекта. Посмотрим для примера на рис.4.8. Астероид на нем в 2 раза больше, чем на рис.4.7. Это во многом объясняет принципы масштабирования. Все, что нам надо сделать, это умножить координаты каждой из вершин объекта на коэффициент масштабирования. Фрагмент кода в Листинге 4.6 показывает, как это делается для структуры объекта, определенной нами ранее. Листинг 4.6. Масштабирование астероида. ------------------------------------------------------------------------- void Scale_Object(object_ptr object,float scale) { int index; // для всех вершин масштабируем х- и у-компоненты for (index = 0; indexnum_vertices; index++) { object->vertices[index].x *= scale; object->vertices[index].y *= scale; } // конец цикла for } // конец функции __________________________________________________________________________ - 93 - │ось Y ┼ ┼ (-7,11) ┼ . ┼ ┼ (8,7) ┼ . ┼ ┼ ┼ ось Х ───────────┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼─┼───────── ┼ ┼ ┼ . . (17,-6) ┼ (4,-6) ┼ . . ┼ (12,-10) (-8,-12) ┼ ┼ │ Рис.4.8. Большой Астероид. Функция из Листинга 4.6 работает путем масштабирования координат каждой из вершин объекта. Если нам придет в голову увеличить наш объект "астероид" в 2 раза, то нам потребуется написать следующее: Scale_Object((object_ptr)&asteroid,2,0); С этим, вроде, все. Теперь мы уже готовы приступить к вращению объекта. Вращение объектов ------------------------ Для того, чтобы вращать объект, мы должны повернуть его вокруг одной из координат. В двухмерной графике для этого обычно выбирается ось Z. Пока мы находимся в двухмерном мире, нас не беспокоит третье измерение - мы просто не придаем ему значения. - 94 - Если экран - это плоскость X-Y, то ось Z - это перпендикуляр к осям X и Y. Таким образом, если мы описываем наши объекты относительно двухмерного мира, то у нас появляется возможность вращать их осносительно оси Z. Следующие формулы позволяют вращать произвольную точку (X,Y) относительно оси Z: new_x = x*cos(angle) - y*sin(angle) new_y = y*cos(angle) + y*sin(angle) где: angle - это угол, на который вы хотите повернуть точку. Кроме этого вам стоит помнить еще пару вещей: - Положительные углы имеют эффект вращения по часовой стрелке; - Отрицательные углы имеют эффект вращения против часовой стрелки. Надо также не забывать, что Си использует для своих функций радианы, а не градусы, и все вызовы тригонометрических функций должны передавать в параметрах также радианы. Для того, чтобы перевести радианы в градусы, мы должны написать простые макросы. Deg_To_Rad(deg) {pi*deg/180;} Rad_To_Deg(rad) {180*rad/pi;} Другими словами, это значит, что в круге 360 градусов или 2хPi радиан. Теперь нам нужно написать функцию для вращения объекта. Давайте просто используем формулы, не задумываясь о том, как и почему они работают. Функция в Листинге 4.7 делает именно то, что мы хотим. Листинг 4.7.Вращение объекта. -------------------------------------------------------------------------- void Rotate_Object(object_ptr object, float angle) { int index; float x_new, y_new,cs,sn; // pre-compute sin and cos cs = cos(angle); sn = sin(angle); // for each vertex rotate it by angle for (index=0; indexnum_vertices; index++) { // rotate the vertex x_new = object->vertices[index].x * cs - object->vertices[index].y * sn; y_new = object->vertices[index].y * cs + object->vertices[index].x * sn; // store the rotated vertex back into structure object->vertices[index].x = x_new; object->vertices[index].y = y_new; } // end for index } // end Rotate_Object ------------------------------------------------------------------------- - 95 - Думаю, что надо кое-что объяснить. Я вычисляю заранее значения синуса и косинуса для данного угла. Зачем, спросите вы. Ответ прост - для скорости. Ибо заниматься вычислениями тригонометрических функций в процессе работы программы можно позволить себе только, имея математический сопроцессор. Теперь настало время написать что-нибудь посерьезней. Мне кажется, что надо бы написать что-то более экстравагантное, чем одинокий астероид. Пусть это будет хотя бы несколько астероидов. Давайте сначала спланируем наши дальнейшие действия. Я хотел бы иметь поле астероидов различных размеров в количестве более 100 штук. И так, чтобы они могли вращаться. Для этого программа должна иметь следующую стректуру: Шаг 1. - Инициировать поле астероидов; Шаг 2. - Стереть поле астероидов; Шаг 3. - Трансформировать поле астероидов; Шаг 4. - Нарисовать поле астероидов; Шаг 5. - Перейти к Шагу 2, пока пользователь не нажмет кнопку. Чтобы сделать это проще, я добавил 3 новых поля к нашей структуре: одно для угла поворота и два - для скорости (целиком программа представлена в Листинге 4.8). Листинг 4.8. Программа, которая рисует поле астероидов (FIELD.C). -------------------------------------------------------------------------- // I N C L U D E S /////////////////////////////////////////////////////////// #include // include the basics #include // include Microsofts Graphics Header #include // include math stuff // D E F I N E S /////////////////////////////////////////////////////////// #define NUM_ASTEROIDS 10 #define ERASE 0 #define DRAW 1 // T Y P E D E F S /////////////////////////////////////////////////////////// // the structure for a vertex typedef struct vertex_typ { float x,y; // a single point in the 2-D plane. } vertex, *vertex_ptr; // the structure for an object typedef struct object_typ { int num_vertices; // number of vertices in this object int color; // color of object float xo,yo; // position of object float x_velocity; // x velocity of object float y_velocity; // y velocity of object float scale; // scale factor float angle; // rotation rate vertex vertices[16]; // 16 vertices } object, *object_ptr; // G L O B A L S ///////////////////////////////////////////////////////////// object asteroids[NUM_ASTEROIDS]; // F U N C T I O N S ///////////////////////////////////////////////////////// void Delay(int t) { // take up some compute cycles float x = 1; while(t-->0) x=cos(x); } // end Delay ////////////////////////////////////////////////////////////////////////////// void Scale_Object(object_ptr object,float scale) { int index; // for all vertices scale the x and y component for (index = 0; indexnum_vertices; index++) { object->vertices[index].x *= scale; object->vertices[index].y *= scale; } // end for index } // end Scale_Object ////////////////////////////////////////////////////////////////////////////// void Rotate_Object(object_ptr object, float angle) { int index; float x_new, y_new,cs,sn; // pre-compute sin and cos cs = cos(angle); sn = sin(angle); // for each vertex rotate it by angle for (index=0; indexnum_vertices; index++) { // rotate the vertex x_new = object->vertices[index].x * cs - object->vertices[index].y * sn; y_new = object->vertices[index].y * cs + object->vertices[index].x * sn; // store the rotated vertex back into structure object->vertices[index].x = x_new; object->vertices[index].y = y_new; } // end for index } // end Rotate_Object ////////////////////////////////////////////////////////////////////////////// void Create_Field(void) { int index; for (index=0; index 600 || asteroids[index].xo < 40) { asteroids[index].x_velocity = -asteroids[index].x_velocity; asteroids[index].xo += asteroids[index].x_velocity; } if (asteroids[index].yo > 440 || asteroids[index].yo < 40) { asteroids[index].y_velocity = -asteroids[index].y_velocity; asteroids[index].yo += asteroids[index].y_velocity; } } // end for index } // end Translate_Asteroids ////////////////////////////////////////////////////////////////////////////// void Rotate_Asteroids(void) { int index; for (index=0; indexelem[i][j]+=matrix_1->elem[i][k] * matrix_2->elem[k][j]; } // конец цикла по k } // конец цикла по j } // конец цикла по i } // конец функции _________________________________________________________________________ Перед выходом из этой функции мы имеем результат, сохраненный в переменной result. Единичная матрица ------------------------ Прежде чем закончить говорить о матрицах, скажем еще об одной вещи: о единичной матрице. Не углубляясь в математические термины, я хочу сказать, что нам нужна такая матрица, умножая на которую мы получали бы исходную матрицу. Говоря попросту, нам нужно иметь матрицу размерностью mxn, которую назовем матрицей I. Умножая на нее любую другую матрицу, мы должны получить исходную. Этой матрицей будет квадратная матрица, по главной диагонали которой записаны единицы, а все остальные элементы равны нулю: │1 0 0 │ I = │0 1 0 │ │0 0 1 │ Если мы умножим матрицу A на матрицу I, - 106 - │1 0 2│ │1 0 0│ A = │3 -1 4│ I = │0 1 0│ │6 -2 0│ │0 0 1│ то результатом будет исходная матрица A: A x I = A ИСПОЛЬЗОВАНИЕ МАТРИЦ В ИГРАХ Мы уже достаточно поговорили о матрицах. Теперь посмотрим, зачем они нам нужны и как их использовать в играх. В программе из Листинга 4.8 мы создали поле астероидов, которые перемещались, изменяли свой размер и вращались. Если мы представим наши объекты как набор матриц, то сможем использовать умножение матриц для их трансформации. Прелесть матриц состоит в том, что вы можете объединить все операции в одну матрицу, описывающую перемещение, масштабирование и вращение. Помните, когда пишутся видеоигры, мы должны искать наиболее быстрый и эффективный алгоритм. Чем больще хитростей и трюков у нас в запасе, тем лучше. Теперь посмотрим на матрицы для перемещения, масштабирования и вращения. Главная матрица перемещений ---------------------------------- Главной матрицей перемещений будем называть такую матрицу, в которой x_translation и y_translation - это коэффициенты перемещения объекта по осям X и Y. Вот как она выглядит: │ │ │1 0 0│ │0 1 0│ │x_translation y_translation 1│ │ │ Главная матрица масштабирования -------------------------------------- Главная матрица масштабирования - это такая матрица, в которой scale_x и scale_y - это коэффициенты масштабирования объекта по координатам x и y: │scale_x 0 0│ │0 scale_y 0│ │0 0 1│ Такая матрица позволяет выполнять неоднородное масштабирование - мы можем задать один масштаб по оси X и другой - по оси Y. Таким образом, если мы хотим масштабировать объект однородно, то должны задать scale_x = scale_y. - 107 - Главная матрица поворотов ------------------------------- В главной матрице поворотов angle - это угол, на который вы хотите повернуть объект: │cos(angle) -sin(angle) 0│ │sin(angle) cos(angle) 0│ │0 0 1│ Общая матрица масштабирования, поворотов и перемещений ---------------------------------------------------------- Наступает торжественный момент. Теперь мы возьмем матрицы перемещения, масштабирования и поворота и перемножим их (получим их конкатенацию), чтобы получить общую матрицу, реализующую все три функции сразу. Окончательно матрица будер выглядеть так: │scale_x x cos(angle) scale_x x -sin(angle) 0│ │scale_y x sin(angle) scale_y x cos(angle) 0│ │x_translation y_translation 1│ Если вы теперь умножите вершины объекта на эту матрицу, то получите перемещенный, повернутый и масштабированный объект. Не слабо, а? Компонент нормализации вершины ------------------------------------ Я совсем упустил одну маленькую деталь, которую вы уже, наверное, заметили. "Как мы можем умножить вершину на матрицу размером 3х3?" Неплохой вопрос. Рад, что вы спросили. Чтобы выполнить это, мы должны изменить представление структуры вершин, добавив компонент нормализации. Компонент нормализации - это просто единица, добавленная в конец структуры, описывающей каждую вершину. Для этого нам надо чуть-чуть изменить исходные тексты в Листинге 4.4, в котором описаны вершины. Все это отражено в структуре данных в Листинге 4.10. Листинг 4.10. Новая структура данных для вершин. ------------------------------------------------------------------------ #define X_COMP 0 #define Y_COMP 1 #define N_COMP 2 typedef struct vertex_typ { float p[3]; } vertex, *vertex_ptr; __________________________________________________________________________ - 108 - Как вы можете заметить, теперь мы превратили матрицу P, описывающую вершину, в массив. Наша матрица преобразований имеет размер 3х3, и для умножения нам надо иметь матрицу размером 1х3. Матрица P удовлетворяет этому условию. Программа Астероиды с использованием матриц --------------------------------------------------- Я уже устал от разговоров - давайте что-нибудь напишем. К примеру, перепишем нашу программу из Листинга 4.8. Вы должны ее помнить. Мы ее переделаем и используем в ней матрицы. Листинг 4.11 показывает эту программу. Она называется "Супер Астероиды". Листинг 4.11. Супер Астероиды (FIELD_DL.C). ------------------------------------------------------------------------- // I N C L U D E S /////////////////////////////////////////////////////////// #include // include the basics #include // include Microsofts Graphics Header #include // include math stuff // D E F I N E S /////////////////////////////////////////////////////////// #define NUM_ASTEROIDS 10 #define ERASE 0 #define DRAW 1 #define X_COMP 0 #define Y_COMP 1 #define N_COMP 2 // T Y P E D E F S /////////////////////////////////////////////////////////// // new and improved vertex typedef struct vertex_typ { float p[3]; // a single point in the 2-D plane with normalizing factor } vertex, *vertex_ptr; // a general matrix structure typedef struct matrix_typ { float elem[3][3]; // storage for a 3x3 martrix } matrix, *matrix_ptr; // the structure for an object typedef struct object_typ { int num_vertices; // number of vertices in this object int color; // color of object float xo,yo; // position of object float x_velocity; // x velocity of object float y_velocity; // y velocity of object matrix scale; // the object scaling matrix matrix rotation; // the objects rotation and translation matrix vertex vertices[16]; // 16 vertices } object, *object_ptr; // G L O B A L S ///////////////////////////////////////////////////////////// object asteroids[NUM_ASTEROIDS]; // F U N C T I O N S ///////////////////////////////////////////////////////// void Delay(int t) { // take up some compute cycles float x = 1; while(t-->0) x=cos(x); } // end Delay ////////////////////////////////////////////////////////////////////////////// void Make_Identity(matrix_ptr i) { // makes the sent matrix into an identity matrix i->elem[0][0] = i->elem[1][1] = i->elem[2][2] = 1; i->elem[0][1] = i->elem[1][0] = i->elem[1][2] = 0; i->elem[2][0] = i->elem[0][2] = i->elem[2][1] = 0; } // end Make_Identity ////////////////////////////////////////////////////////////////////////////// void Clear_Matrix(matrix_ptr m) { // zeros out the sent matrix m->elem[0][0] = m->elem[1][1] = m->elem[2][2] = 0; m->elem[0][1] = m->elem[1][0] = m->elem[1][2] = 0; m->elem[2][0] = m->elem[0][2] = m->elem[2][1] = 0; } // end Clear_Matrix ////////////////////////////////////////////////////////////////////////////// void Mat_Mul(vertex_ptr v,matrix_ptr m) { // do a multiplication of a 1x3 * 3x3 the result is again a 1x3 // for speed manually do the multiplication by specifying each multiplication // and addition manually (apprentice trick) float x_new, y_new; x_new = v->p[0]*m->elem[0][0] + v->p[1]*m->elem[1][0] + m->elem[2][0]; y_new = v->p[0]*m->elem[0][1] + v->p[1]*m->elem[1][1] + m->elem[2][1]; v->p[X_COMP] = x_new; v->p[Y_COMP] = y_new; // note we need not change N_COMP since it is always 1 } // end Mat_Mul ////////////////////////////////////////////////////////////////////////////// void Scale_Object_Mat(object_ptr obj) { int index; // scale the object, just multiply each point in the object by it's scaling // matrix for (index=0; indexnum_vertices; index++) { Mat_Mul((vertex_ptr)&obj->vertices[index],(matrix_ptr)&obj->scale); } // end for index } // end Scale_Oject_Mat ////////////////////////////////////////////////////////////////////////////// Rotate_Object_Mat(object_ptr obj) { int index; // rotate the object, just multiply each point in the object by it's rotation // matrix for (index=0; indexnum_vertices; index++) { Mat_Mul((vertex_ptr)&obj->vertices[index],(matrix_ptr)&obj->rotation); } // end for index } // end Rotate_Oject_Mat ////////////////////////////////////////////////////////////////////////////// void Create_Field(void) { int index; float angle,c,s; // this function creates the asteroid field for (index=0; index 600 || asteroids[index].xo < 40) { asteroids[index].x_velocity = -asteroids[index].x_velocity; asteroids[index].xo += asteroids[index].x_velocity; } if (asteroids[index].yo > 440 || asteroids[index].yo < 40) { asteroids[index].y_velocity = -asteroids[index].y_velocity; asteroids[index].yo += asteroids[index].y_velocity; } } // end for index } // end Translate_Asteroids ////////////////////////////////////////////////////////////////////////////// void Rotate_Asteroids() { int index; for (index=0; index